﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;

namespace AJAXChatClient
{
    public static class AjaxChatHelpers
    {
        private static Regex splitLongMessageRE1 = new Regex(
            @"\[\w+(?:=[^\[\]<>]*?)?\]|\[/\w+\]",
            RegexOptions.Singleline | RegexOptions.RightToLeft);
        private static Regex splitLongMessageRE2 = new Regex(
            @"<\w+(?:\s[^<>]+?)?>|</\w+>",
            RegexOptions.Singleline | RegexOptions.RightToLeft);
        private static Regex splitLongMessageRE3 = new Regex(
            @"(\[(\w+)(?:=([^\[\]<>]*?))?\](.*?)\[/\2\])|" + // 1:BBCode match, 2:tag, 3:attr, 4:content
            @"(\[(\w+)(?:=([^\[\]<>]*?))?\])|" + // 5:BBCode match, 6:tag, 7:attr
            @"(<(\w+)(?:\s([^<>]+?))?>(.*?)</\9>)|" + // 8:HTML match, 9:tag, 10:attrs, 11: content
            @"(<(\w+)(?:\s([^<>]+?))?>)", // 12:HTML match, 13:tag, 14: attrs
            RegexOptions.Singleline | RegexOptions.Compiled);

        private class AddClosingTagsEvaluator
        {
            private string suffix;
            private readonly MatchEvaluator evaluator;

            public AddClosingTagsEvaluator()
            {
                this.suffix = null;
                this.evaluator = new MatchEvaluator(this.addClosingTagsCallback);
            }

            public string Evaluate(string text)
            {
                this.suffix = "";
                return splitLongMessageRE3.Replace(text, this.evaluator) + this.suffix;
            }

            private string addClosingTagsCallback(Match match)
            {
                string tag, result = "";
                Group attr;
                if (match.Groups[1].Success)
                {
                    // Full BB Code Tag Block)
                    tag = match.Groups[2].Value;
                    attr = match.Groups[3];
                    result = attr.Success
                        ? string.Concat("[", tag, "=", attr.Value, "]")
                        : string.Concat("[", tag, "]");
                    return result
                        + AddClosingTags(match.Groups[4].Value)
                        + string.Concat("[/", tag, "]");
                }
                if (match.Groups[5].Success)
                {
                    // Unclosed BB Code Tag
                    tag = match.Groups[6].Value;
                    attr = match.Groups[7];
                    result = attr.Success
                        ? string.Concat("[", tag, "=", attr.Value, "]")
                        : string.Concat("[", tag, "]");
                    this.suffix = string.Concat("[/", tag, "]", this.suffix);
                    return result;
                }
                if (match.Groups[8].Success)
                {
                    // Full HTML Tag Block
                    tag = match.Groups[9].Value;
                    attr = match.Groups[10];
                    result = attr.Success
                        ? string.Concat("<", tag, "=", attr.Value, "]")
                        : string.Concat("[", tag, "]");
                    return result
                        + AddClosingTags(match.Groups[11].Value)
                        + string.Concat("</", tag, ">");
                }
                if (match.Groups[12].Success)
                {
                    // Unclosed HTML Tag
                    tag = match.Groups[13].Value;
                    attr = match.Groups[14];
                    if (attr.Success)
                    {
                        // Check if HTML Tag is self-closing
                        if (attr.Value[attr.Length - 1] != '/')
                            this.suffix = string.Concat("</", tag, ">", this.suffix);
                        result = string.Concat("<", tag, " ", attr.Value, ">");
                    }
                    else
                    {
                        result = string.Concat("<", tag, ">");
                        this.suffix = string.Concat("</", tag, ">", this.suffix);
                    }
                    return result;
                }
                return match.Value;
            }
        }

        public static string AddClosingTags(string text)
        {
            if (string.IsNullOrEmpty(text))
            {
                return text;
            }
            AddClosingTagsEvaluator eval = new AddClosingTagsEvaluator();
            return eval.Evaluate(text);
        }

        private static string[] SplitLongMessage(string text, int maxLength, bool alwaysCloseTags)
        {
            if (string.IsNullOrEmpty(text))
            {
                return new string[] { text };
            }
            if (maxLength <= 0)
            {
                if (alwaysCloseTags)
                {
                    AddClosingTagsEvaluator eval = new AddClosingTagsEvaluator();
                    text = eval.Evaluate(text);
                }
                return new string[] { text };
            }
            List<string> messages = new List<string>(text.Length / maxLength + 2);
            string message, tag, suffix;
            string prefix = "";
            bool withinCode = false;
            bool splitAgain = false;
            char c;
            int i = 0;
            int j, k, length, index, matchCount;
            Group group;
            Match match;
            MatchCollection matches;
            System.Collections.IEnumerator matchEnum;
            // MAIN LOOP: Splits text into messages with lengths <= maxLength
            while (i < text.Length)
            {
                // If the remaining text's length is <= maxLength,
                // there is no immediate need to split it again,
                // as we already have leeway for the prefix and suffix.
                splitAgain = (i + maxLength) <= text.Length;
                length = splitAgain ? maxLength : text.Length - i;
                // Compensate for the length of the prefix from the previous iteration
                if ((length + prefix.Length) > maxLength)
                {
                    length = maxLength - prefix.Length;
                    splitAgain = true;
                }
                message = text.Substring(i, length);
                // Get rid of any tags that might have been split internally 
                // at the end of the message substring
                c = message[length - 1];
                if (c != ']' && c != '>')
                {
                    index = message.LastIndexOf('[');
                    if (index >= 0)
                    {
                        // Check whether the last '[' belongs to
                        // the last intact BB Code Tag
                        match = splitLongMessageRE1.Match(message);
                        if (!match.Success || match.Index != index)
                        {
                            length = index;
                            message = message.Substring(0, index);
                        }
                    }
                    index = message.LastIndexOf('<');
                    if (index >= 0)
                    {
                        // Check whether the last '<' belongs to
                        // the last intact HTML Tag
                        match = splitLongMessageRE2.Match(message);
                        if (!match.Success || match.Index != index)
                        {
                            length = index;
                            message = message.Substring(0, index);
                        }
                    }
                }
                // Append prefix from the previous iteration
                index = length;
                message = string.Concat(prefix, message);
                length = message.Length;
                // Scan the message for unclosed tags. For each found, 
                // Add a closing tag in the suffix and
                // Copy the tag over to the prefix for the next message,
                // in case splitting it left orphaned closing tags.
                prefix = "";
                suffix = "";
                withinCode = false;
                matches = splitLongMessageRE3.Matches(message);
                matchEnum = matches.GetEnumerator();
                matchCount = 0;
                while (matchEnum.MoveNext())
                {
                    match = matchEnum.Current as Match;
                    matchCount++;
                    if (match.Index > length)
                    {
                        // The <tag> or <tag>content</tag> is beyond the length
                        // of the message, and any following matches will be as well,
                        // so cut off the message at or before its length.
                        splitAgain = true;
                        // Prevent from evaluating matches entirely outside the new length.
                        matchCount--;
                        // It's getting split here, so don't find anymore matches.
                        break;
                    }
                    else if (splitAgain && match.Index <= length &&
                        (match.Index + match.Length) >= length)
                    {
                        // The <tag> or <tag>content</tag> intersects the new length,
                        // so cut off the message at the start of the opening tag.
                        index -= length - match.Index;
                        length = match.Index;
                        // Since the message is split at the start of an opening tag,
                        // there is no need to further evaluate where to split it, 
                        // unless it ends with a partial code block that needs to be 
                        // split at a line break.
                        splitAgain = false;
                        if (withinCode)
                        {
                            splitAgain = message[length - 1] != '\n';
                            if (!splitAgain)
                            {
                                // Do quick evaluation here
                                index--; // Include '\n' in next message
                                length--; // Exclude '\n' from message end
                            }
                        }
                        // It's getting split here, so don't find anymore matches.
                        break;
                    }
                    else if (match.Groups[5].Success)
                    {
                        // Unclosed BB Code Tag found
                        tag = match.Groups[6].Value;
                        prefix = string.Concat(prefix, match.Groups[5].Value);
                        suffix = string.Concat("[/", tag, "]", suffix);
                        if ("code".Equals(tag, StringComparison.OrdinalIgnoreCase))
                        {
                            withinCode = true;
                        }
                        if (splitAgain)
                        {
                            index -= tag.Length + 3;
                            length -= tag.Length + 3;
                        }
                        else if ((length + suffix.Length) > maxLength)
                        {
                            index -= length - (maxLength - suffix.Length);
                            length = maxLength - suffix.Length;
                            splitAgain = true;
                        }
                    }
                    else if (match.Groups[12].Success)
                    {
                        // Check for self-closing HTML tag (<tag/>)
                        group = match.Groups[14];
                        if (group.Length == 0 || message[group.Index + group.Length - 1] != '/')
                        {
                            // Unclosed HTML Tag found
                            tag = match.Groups[13].Value;
                            prefix = string.Concat(prefix, match.Groups[12].Value);
                            suffix = string.Concat("</", tag, ">", suffix);
                            if ("code".Equals(tag, StringComparison.OrdinalIgnoreCase))
                            {
                                withinCode = true;
                            }
                            if (splitAgain)
                            {
                                index -= tag.Length + 3;
                                length -= tag.Length + 3;
                            }
                            else if ((length + suffix.Length) > maxLength)
                            {
                                index -= length - (maxLength - suffix.Length);
                                length = maxLength - suffix.Length;
                                splitAgain = true;
                            }
                        }
                    }
                }
                // Find an appropriate place to cut off the message so words
                // and code lines aren't split between messages.
                if (splitAgain)
                {
                    // Code is split at line breaks instead of word boundaries
                    // in order to make it readable across multiple messages.
                    if (withinCode)
                    {
                        k = message.LastIndexOf('\n', length - 1, length);
                        for (j = matchCount - 1; j >= 0 && withinCode; j--)
                        {
                            match = matches[j];
                            while (k > match.Index && withinCode)
                            {
                                // Line break is within or after the last match
                                // Check if it's in a location that can be split
                                // between this message and the next
                                if ((match.Index + match.Length) <= k)
                                {
                                    // Line break is after the last match
                                    // Cut the message off at the line break
                                    index -= length - k + 1; // Include '\n' in next message
                                    length = k - 1; // Exclude '\n' from message end
                                    withinCode = false;
                                }
                                else if (match.Groups[1].Success)
                                {
                                    // Match is a closed BB Code tag block
                                    // Check if the line break is within its content
                                    group = match.Groups[4];
                                    if (group.Index <= k &&
                                       (group.Index + group.Length) >= k)
                                    {
                                        // Split the tag block's content at the line break
                                        tag = match.Groups[2].Value;
                                        group = match.Groups[3];
                                        prefix = string.Concat(prefix, group.Success
                                            ? string.Concat("[", tag, "=", group.Value, "]")
                                            : string.Concat("[", tag, "]"));
                                        suffix = string.Concat("[/", tag, "]", suffix);
                                        index -= length - k + 1; // Include '\n' in next message
                                        length = k - 1; // Exclude '\n' from message end
                                        withinCode = false;
                                    }
                                    else
                                    {
                                        // The line break is within a tag in the match
                                        // Move on to the next line break character
                                        k = message.LastIndexOf('\n', k - 1, k);
                                    }
                                }
                                else if (match.Groups[8].Success)
                                {
                                    // Match is a closed HTML tag block
                                    // Check if the line break is within its content
                                    group = match.Groups[11];
                                    if (group.Index <= k &&
                                       (group.Index + group.Length) >= k)
                                    {
                                        // Split the tag block's content at the line break
                                        tag = match.Groups[9].Value;
                                        group = match.Groups[10];
                                        prefix = string.Concat(prefix, group.Success
                                            ? string.Concat("<", tag, " ", group.Value, ">")
                                            : string.Concat("<", tag, ">"));
                                        suffix = string.Concat("</", tag, ">", suffix);
                                        index -= length - k + 1; // Include '\n' in next message
                                        length = k - 1; // Exclude '\n' from message end
                                        withinCode = false;
                                    }
                                    else
                                    {
                                        // The line break is within a tag in the match
                                        // Move on to the next line break character
                                        k = message.LastIndexOf('\n', k - 1, k);
                                    }
                                }
                                else
                                {
                                    // The line break is within a tag in the match
                                    // Move on to the next line break character
                                    k = message.LastIndexOf('\n', k - 1, k);
                                }
                            }
                        }
                    }
                    else
                    {
                        c = message[length - 1];
                        while (!char.IsWhiteSpace(c))
                        {
                            index--;
                            length--;
                            c = message[length - 1];
                        }
                        // Exclude whitespaces from message end
                        while (char.IsWhiteSpace(c))
                        {
                            length--;
                            c = message[length - 1];
                        }
                    }
                }
                message = message.Substring(0, length);
                messages.Add(string.Concat(message, suffix));
                i += index;
            }
            return messages.ToArray();
        }

        public static string EncodeText(string text)
        {
            // difference between EscapeUriString and EscapeDataString?
            return Uri.EscapeDataString(text);
        }

        public static string DecodeText(string text)
        {
            return Uri.UnescapeDataString(text);
        }

        public static string Utf8Encode(string plainText)
        {
            string utf8Text = "";
            for (int i = 0; i < plainText.Length; i++)
            {
                ushort c = (ushort)plainText[i];
                if (c < 128)
                {
                    utf8Text += (char)c;
                }
                else if ((c > 127) && (c < 2048))
                {
                    utf8Text += (char)((c >> 6) | 192);
                    utf8Text += (char)((c & 63) | 128);
                }
                else
                {
                    utf8Text += (char)((c >> 12) | 224);
                    utf8Text += (char)(((c >> 6) & 63) | 128);
                    utf8Text += (char)((c & 63) | 128);
                }
            }
            //byte[] data = Encoding.UTF8.GetBytes(plainText);
            //string utf8Text = Encoding.ASCII.GetString(data);
            return utf8Text;
        }

        public static string Utf8Decode(string utf8Text)
        {
            string plainText = "";
            ushort c, c2, c3;
            int i = 0;
            while (i < utf8Text.Length)
            {
                c = (ushort)utf8Text[i];
                if (c < 128)
                {
                    plainText += (char)c;
                    i++;
                }
                else if ((c > 191) && (c < 224))
                {
                    c2 = (ushort)utf8Text[i + 1];
                    plainText += (char)(((c & 31) << 6) | (c2 & 63));
                    i += 2;
                }
                else
                {
                    c2 = (ushort)utf8Text[i + 1];
                    c3 = (ushort)utf8Text[i + 2];
                    plainText += (char)(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
                }
            }
            //byte[] data = Encoding.ASCII.GetBytes(utf8Text);
            //string plainText = Encoding.UTF8.GetString(data);
            return plainText;
        }

        private static Regex encodeSpecialCharsRE;

        public static string EncodeSpecialChars(string text)
        {
            if (encodeSpecialCharsRE == null)
            {
                encodeSpecialCharsRE = new Regex("[&<>'\"]", RegexOptions.None);
            }
            return encodeSpecialCharsRE.Replace(text, encodeSpecialCharsCallback);
        }

        private static string encodeSpecialCharsCallback(Match match)
        {
            switch (match.Value)
            {
                case "&":
                    return "&amp;";
                case "<":
                    return "&lt;";
                case ">":
                    return "&gt;";
                case "'":
                    // As &apos; is not supported by IE, we use &#39; as replacement for ('):
                    return "&#39;";
                case "\"":
                    return "&quot;";
                default:
                    return match.Value;
            }
        }

        private static Regex decodeSpecialCharsRE;

        public static string DecodeSpecialChars(string text)
        {
            if (decodeSpecialCharsRE == null)
            {
                decodeSpecialCharsRE = new Regex("(&amp;)|(&lt;)|(&gt;)|(&quot;)", RegexOptions.None);
            }
            return decodeSpecialCharsRE.Replace(text, decodeSpecialCharsCallback);
        }

        private static string decodeSpecialCharsCallback(Match match)
        {
            switch (match.Value)
            {
                case "&amp;":
                    return "&";
                case "&lt;":
                    return "<";
                case "&gt;":
                    return ">";
                case "&#39;":
                    return "'";
                case "&quot;":
                    return "\"";
                default:
                    return match.Value;
            }
        }

        private static Regex stripTagsRE;

        public static string StripTags(string str)
        {
            if (stripTagsRE == null)
            {
                stripTagsRE = new Regex("</?[^>]+?>", RegexOptions.None);
            }
            return stripTagsRE.Replace(str, "");
        }

        private static Regex stripBBCodeTagsRE;

        public static string StripBBCodeTags(string str)
        {
            if (stripBBCodeTagsRE == null)
            {
                stripBBCodeTagsRE = new Regex(@"\[/?[^\]]+?\]", RegexOptions.None);
            }
            return stripBBCodeTagsRE.Replace(str, "");
        }

        private static Regex escapeRegExpRE;

        public static string EscapeRegExp(string text)
        {
            if (escapeRegExpRE == null)
            {
                string[] specials = new string[] {
                    "^", "$", "*", "+", "?", ".", "|", //"/",
                    "(", ")", "[", "]", "{", "}", "\\"
                };
                escapeRegExpRE = new Regex(
                    "(\\" + string.Join("|\\", specials) + ")", 
                    RegexOptions.None
                );
            }
            return escapeRegExpRE.Replace(text, "$1");
        }

        public static string AddSlashes(string text)
        {
            // Adding slashes in front of apostrophs and backslashes to ensure a valid JavaScript expression:
            return text.Replace("\\", "\\\\").Replace("'", "\\'");
        }

        public static string RemoveSlashes(string text)
        {
            // Removing slashes added by calling addSlashes(text) previously:
            return text.Replace("\\\\", "\\").Replace("\\'", "'");
        }

        public static string FormatDate(string format, DateTime date)
        {
            //date = (date == null) ? DateTime.Now : date;
            return format
                .Replace("%Y", date.Year.ToString())
                .Replace("%m", addLeadingZero(date.Month))
                .Replace("%d", addLeadingZero(date.Day))
                .Replace("%H", addLeadingZero(date.Hour))
                .Replace("%i", addLeadingZero(date.Minute))
                .Replace("%s", addLeadingZero(date.Second));
        }

        private static string addLeadingZero(int number)
        {
            string num = number.ToString();
            if (num.Length < 2)
            {
                num = "0" + num;
            }
            return num;
        }

        public static string GetClass(HtmlElement node)
        {
            return node.GetAttribute("class");
        }

        public static void SetClass(HtmlElement node, string className)
        {
            node.SetAttribute("class", className);
        }

        public static string ScriptLinkEncode(string text)
        {
            return EncodeText(AddSlashes(DecodeSpecialChars(text)));
        }

        public static string ScriptLinkDecode(string text)
        {
            return EncodeSpecialChars(RemoveSlashes(DecodeText(text)));
        }

        private static bool? utf8DecodeFlag;

        private class navigator
        {
            public static string appName;
        }

        public static string GetScriptLinkValue(string value)
        {
            // This method returns plainText encoded values from javascript links
            // The value has to be utf8Decoded for MSIE and Opera:
            if (utf8DecodeFlag == null)
            {
                switch (navigator.appName)
                {
                    case "Microsoft Internet Explorer":
                    case "Opera":
                        utf8DecodeFlag = true;
                        return AjaxChatHelpers.Utf8Decode(value);
                    default:
                        utf8DecodeFlag = false;
                        return value;
                }
            }
            else if (utf8DecodeFlag.Value)
            {
                return AjaxChatHelpers.Utf8Decode(value);
            }
            else
            {
                return value;
            }
        }

        private static Regex regExpOpenTags;
        private static Regex regExpCloseTags;

        public static bool ContainsUnclosedTags(string str)
        {
            if (regExpOpenTags == null || regExpCloseTags == null)
            {
                regExpOpenTags = new Regex("<[^>/]+?", RegexOptions.Multiline);
                regExpCloseTags = new Regex("</[^>]+?>", RegexOptions.Multiline);
            }
            MatchCollection openTags = regExpOpenTags.Matches(str);
            MatchCollection closeTags = regExpCloseTags.Matches(str);
            // Return true if the number of tags doesn't match:
            if ((openTags == null && closeTags != null) ||
                (openTags != null && closeTags == null) ||
                (openTags != null && closeTags != null && openTags.Count != closeTags.Count))
            {
                return true;
            }
            return false;
        }

        private const string breakString = "&#8203;";

        public static string BreakLongWords(string text, int maxWordLength)
        {
            string newText = "";
            int charCounter = 0;
            char currentChar;
            bool withinTag = false;
            bool withinEntity = false;

            for (int i = 0; i < text.Length; i++)
            {
                currentChar = text[i];

                // Check if we are within a tag or entity
                if (currentChar == '<')
                {
                    withinTag = true;
                    // Reset the charCounter after newline tags (<br/>):
                    if (i > 5 && text.Substring(i - 5, 4) == "<br/")
                        charCounter = 0;
                }
                else if (withinTag && i > 0 && text[i - 1] == '>')
                {
                    withinTag = false;
                    // Reset the charCounter after newline tags (<br/>):
                    if (i > 4 && text.Substring(i - 5, 4) == "<br/")
                        charCounter = 0;
                }

                if (!withinTag && currentChar == '&')
                {
                    withinEntity = true;
                }
                else if (withinEntity && i > 0 && text[i - 1] == ';')
                {
                    withinEntity = false;
                    // We only increase the charCounter once for the whole entity:
                    charCounter++;
                }

                if (!withinTag && !withinEntity)
                {
                    // Reset the charCounter if we encounter a word boundary:
                    switch (currentChar)
                    {
                        case ' ':
                        case '\n':
                        case '\t':
                            charCounter = 0;
                            break;
                        default:
                            // We are not within a tag or entity, increase the charCounter:
                            charCounter++;
                            break;
                    }
                    if (charCounter > maxWordLength)
                    {
                        // mawWordLength has been reached, break here and reset the charCounter:
                        newText += breakString;
                        charCounter = 0;
                    }
                }
                // Add the current char to the text:
                newText += currentChar;
            }
            return newText;
        }
    }
}
